
package com.tonyodev.fetch2fileserver.provider;

import ohos.eventhandler.EventHandler;

import com.tonyodev.fetch2core.FetchCoreUtils;
import com.tonyodev.fetch2core.FetchLogger;
import com.tonyodev.fetch2core.FileResource;
import com.tonyodev.fetch2core.InputResourceWrapper;
import com.tonyodev.fetch2core.InterruptMonitor;
import com.tonyodev.fetch2core.server.FetchFileResourceTransporter;
import com.tonyodev.fetch2core.server.FileRequest;
import com.tonyodev.fetch2core.server.FileResourceTransporter;
import com.tonyodev.fetch2core.server.FileResponse;
import com.tonyodev.fetch2fileserver.FileResolver;

import java.io.IOException;
import java.net.HttpURLConnection;
import java.net.Socket;
import java.util.Date;
import java.util.UUID;

import static com.tonyodev.fetch2core.server.FileResponse.CLOSE_CONNECTION;
import static com.tonyodev.fetch2core.server.FileResponse.OPEN_CONNECTION;

public class FetchFileResourceProvider implements FileResourceProvider {

    private Socket client;
    private FileResourceProviderDelegate fileResourceProviderDelegate;
    private FetchLogger logger;
    private EventHandler ioHandler;
    private long progressReportingInMillis;
    private long persistentTimeoutInMillis;
    private FileResolver fileResolver;
    private String id = UUID.randomUUID().toString();
    private Object lock = new Object();
    private FileResourceTransporter transporter;

    private boolean interrupted = false;
    private InputResourceWrapper inputResourceWrapper;
    private FileRequest clientRequest;
    private boolean persistConnection = true;
    private InterruptMonitorObject interruptMonitor = new InterruptMonitorObject();
    private Runnable persistentRunnable = new Runnable() {
        @Override
        public void run() {
            interrupted = true;
        }
    };
    private String sessionId = UUID.randomUUID().toString();
    private FileResource fileResource;

    public FetchFileResourceProvider(Socket client, FileResourceProviderDelegate fileResourceProviderDelegate, FetchLogger logger, EventHandler ioHandler, long progressReportingInMillis, long persistentTimeoutInMillis, FileResolver fileResolver) {
        this.client = client;
        this.fileResourceProviderDelegate = fileResourceProviderDelegate;
        this.logger = logger;
        this.ioHandler = ioHandler;
        this.progressReportingInMillis = progressReportingInMillis;
        this.persistentTimeoutInMillis = persistentTimeoutInMillis;
        this.fileResolver = fileResolver;
        transporter = new FetchFileResourceTransporter(client);
    }

    @Override
    public String getId() {
        return id;
    }

    @Override
    public void execute() {

        new Thread(new Runnable() {
            @Override
            public void run() {
                try {
                    while (persistConnection && !interrupted) {
                        ioHandler.postTask(persistentRunnable, persistentTimeoutInMillis);
                        clientRequest = getFileResourceClientRequest();
                        FileRequest request = clientRequest;
                        ioHandler.removeTask(persistentRunnable);
                        if (request != null && !interrupted) {
                            persistConnection = clientRequest.isPersistConnection();
                            if (!interrupted && fileResourceProviderDelegate.acceptAuthorization(sessionId, clientRequest.getAuthorization(), request)) {
                                fileResourceProviderDelegate.onClientConnected(sessionId, request);
                                if (request.getExtras().isNotEmpty()) {
                                    fileResourceProviderDelegate.onClientDidProvideExtras(sessionId, request.getExtras(), request);
                                }
                                switch (request.getType()) {
                                    case FileRequest.TYPE_PING:
                                        if (!interrupted) {
                                            sendPingResponse();
                                        }
                                        break;
                                    case FileRequest.TYPE_CATALOG:
                                        if (!interrupted) {
                                            String catalog = fileResourceProviderDelegate.getCatalog(request.getPage(), request.getSize());
                                            byte[] data = catalog.getBytes("UTF-8");
                                            if (!interrupted) {
                                                long contentLength = (request.getRangeEnd() == -1L ? (long) data.length : request.getRangeEnd()) - request.getRangeStart();
                                                sendCatalogResponse(contentLength, FetchCoreUtils.getMd5String(data));
                                                transporter.sendRawBytes(data, (int) request.getRangeStart(), (int) contentLength);
                                            }
                                        }
                                        break;
                                    case FileRequest.TYPE_FILE:
                                        FileResource fileResource = fileResourceProviderDelegate.getFileResource(request.getFileResourceId());
                                        if (!interrupted) {
                                            if (fileResource != null) {
                                                inputResourceWrapper = fileResourceProviderDelegate.getFileInputResourceWrapper(sessionId, request, fileResource, request.getRangeStart());
                                                if (inputResourceWrapper == null) {
                                                    if (fileResource.getId() == FileRequest.CATALOG_ID) {
                                                        byte[] catalog = fileResource.getExtras().getString("data", "{}").getBytes("UTF-8");
                                                        fileResource.setLength(request.getRangeEnd() == -1L ? catalog.length : request.getRangeEnd());
                                                        fileResource.setMd5(FetchCoreUtils.getMd5String(catalog));
                                                        inputResourceWrapper = fileResolver.getCatalogInputWrapper(catalog, request, fileResource);
                                                    } else {
                                                        inputResourceWrapper = fileResolver.getInputWrapper(fileResource);
                                                        if (inputResourceWrapper != null) {
                                                            inputResourceWrapper.setReadOffset(request.getRangeStart());
                                                        }
                                                    }
                                                }
                                                if (!interrupted) {
                                                    long reportingStopTime;
                                                    byte[] byteArray = new byte[FileResourceTransporter.BUFFER_SIZE];
                                                    long contentLength = request.getRangeEnd() == -1L ? fileResource.getLength() - request.getRangeStart() : request.getRangeEnd() - request.getRangeStart();
                                                    long remainderBytes = contentLength;
                                                    sendFileResourceResponse(contentLength, fileResource.getMd5());
                                                    long reportingStartTime = System.nanoTime();
                                                    int read = inputResourceWrapper != null ? inputResourceWrapper.read(byteArray) : -1;
                                                    int streamBytes;
                                                    fileResourceProviderDelegate.onStarted(sessionId, request, fileResource);
                                                    while (remainderBytes > 0L && read != -1 && !interrupted) {
                                                        if ((long) read <= remainderBytes) {
                                                            streamBytes = read;
                                                        } else {
                                                            read = -1;
                                                            streamBytes = (int) remainderBytes;
                                                        }
                                                        transporter.sendRawBytes(byteArray, 0, streamBytes);
                                                        if (read != -1) {
                                                            remainderBytes -= (long) streamBytes;
                                                            reportingStopTime = System.nanoTime();
                                                            boolean hasReportingTimeElapsed = FetchCoreUtils.hasIntervalTimeElapsed(reportingStartTime,
                                                                reportingStopTime, progressReportingInMillis);
                                                            if (hasReportingTimeElapsed && !interrupted) {
                                                                int progress = FetchCoreUtils.calculateProgress(contentLength - remainderBytes, contentLength);
                                                                fileResourceProviderDelegate.onProgress(sessionId, request, fileResource, progress);
                                                                reportingStartTime = System.nanoTime();
                                                            }

                                                            read = inputResourceWrapper != null ? inputResourceWrapper.read(byteArray) : -1;
                                                        }
                                                    }
                                                    if (remainderBytes == 0L && !interrupted) {
                                                        fileResourceProviderDelegate.onProgress(sessionId, request, fileResource, 100);
                                                        fileResourceProviderDelegate.onComplete(sessionId, request, fileResource);
                                                    }
                                                }
                                                cleanFileStreams();
                                            } else {
                                                sendInvalidResponse(HttpURLConnection.HTTP_NO_CONTENT);
                                            }
                                        }
                                        break;
                                    case FileRequest.TYPE_INVALID:
                                        break;
                                    default:
                                        if (!interrupted) {
                                            fileResourceProviderDelegate.onCustomRequest(sessionId, request, transporter, interruptMonitor);
                                        }
                                        break;
                                }
                            } else if (!interrupted) {
                                System.out.println("FetchFileServerProvider - ClientRequestRejected - ${request.toJsonString}");
                                sendInvalidResponse(HttpURLConnection.HTTP_FORBIDDEN);
                            }
                        } else if (!interrupted) {
                            sendInvalidResponse(HttpURLConnection.HTTP_BAD_REQUEST);
                        }
                        clientRequest = null;
                    }

                } catch (Exception e) {
                    System.out.println("FetchFileServerProvider" + e.getMessage());
                    e.printStackTrace();
                    try {
                        sendInvalidResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
                    } catch (Exception exception) {
                        System.out.println("FetchFileServerProvider" + exception.getMessage());
                        exception.printStackTrace();
                    }
                    if (fileResource != null && clientRequest != null) {
                        fileResourceProviderDelegate.onError(sessionId, clientRequest, fileResource, e);
                    }
                } finally {
                    if (interrupted) {
                        try {
                            sendInvalidResponse(HttpURLConnection.HTTP_INTERNAL_ERROR);
                        } catch (Exception e) {
                            System.out.println("FetchFileServerProvider - ${e.message}" + e.getMessage());
                        }
                    }
                    ioHandler.removeTask(persistentRunnable);
                    transporter.close();
                    cleanFileStreams();
                    fileResource = null;
                    persistConnection = false;
                    clientRequest = null;
                    fileResourceProviderDelegate.onFinished(id);
                }
            }
        }).start();

    }

    private void cleanFileStreams() {
        try {
            if (inputResourceWrapper != null) {
                inputResourceWrapper.close();
            }
        } catch (Exception e) {
            System.out.println("FetchFileServerProvider - ${e.message}" + e.getMessage());
        }
        inputResourceWrapper = null;
    }

    private class InterruptMonitorObject implements InterruptMonitor {
        @Override
        public boolean isInterrupted() {
            return interrupted;
        }
    }

    private FileRequest getFileResourceClientRequest() throws IOException {
        while (!interrupted) {
            FileRequest request = transporter.receiveFileRequest();
            if (request != null) {
                return request;
            }
        }
        return null;
    }

    private void sendPingResponse() {
        FileResponse response = new FileResponse(HttpURLConnection.HTTP_OK,
            FileRequest.TYPE_PING,
            OPEN_CONNECTION,
            new Date().getTime(),
            0,
            "",
            sessionId);
        try {
            transporter.sendFileResponse(response);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void sendInvalidResponse(int status) {
        FileResponse response = new FileResponse(status,
            FileRequest.TYPE_INVALID,
            CLOSE_CONNECTION,
            new Date().getTime(),
            0, "",
            sessionId);
        try {
            transporter.sendFileResponse(response);
        } catch (IOException e) {
            e.printStackTrace();
        }
        interrupted = true;
    }

    private void sendCatalogResponse(long contentLength, String md5) {
        FileResponse response = new FileResponse(
            HttpURLConnection.HTTP_OK,
            FileRequest.TYPE_CATALOG,
            OPEN_CONNECTION,
            new Date().getTime(),
            contentLength,
            md5,
            sessionId);
        try {
            transporter.sendFileResponse(response);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    private void sendFileResourceResponse(long contentLength, String md5) {
        FileResponse response = new FileResponse(HttpURLConnection.HTTP_PARTIAL,
            FileRequest.TYPE_FILE,
            OPEN_CONNECTION,
            new Date().getTime(),
            contentLength,
            md5,
            sessionId);
        try {
            transporter.sendFileResponse(response);
        } catch (IOException e) {
            e.printStackTrace();
        }
    }


    @Override
    public void interrupt() {
        synchronized (lock) {
            interrupted = true;
        }
    }

    @Override
    public boolean isServingFileResource(FileResource fileResource) {
        return fileResource != null ? fileResource.equals(fileResource) : false;
    }
}
